Es el modelo más simple de neurona artificial, propuesto en 1943 por Warren McCulloch y Walter Pitts. Representa una neurona como un sistema lógico binario: las entradas y la salida son 0 o 1.
Suma ponderada:
$$ z = \sum_{i=1}^{n} w_i \cdot x_i $$
Función de activación (threshold o umbral):
$$ y = \begin{cases} 1 & \text{si } z \geq \theta \\ 0 & \text{si } z < \theta \end{cases} $$
| $x_1$ | $x_2$ | $z$ | $y$ |
|---|---|---|---|
| 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 |
| 1 | 0 | 1 | 0 |
| 1 | 1 | 2 | 1 |
✅ Activa solo si ambas entradas son 1.
Claro. Extender la neurona de McCulloch-Pitts a valores continuos implica transicionar desde un modelo discreto y lógico (valores binarios y función umbral) a un modelo realista y diferenciable, base de las redes neuronales modernas (perceptrón o MLP).
![]()
| Aspecto | McCulloch-Pitts | Neurona Continua (Perceptrón) |
|---|---|---|
| Entradas | Binarias (0 o 1) | Reales $x_i \in \mathbb{R}$ |
| Pesos | Fijos o 1 | Reales $w_i \in \mathbb{R}$ |
| Suma | $\sum x_i$ | $z = \sum w_i x_i + b$ |
| Activación | Umbral duro | Función continua y diferenciable |
| Ejemplo de activación | $f(z) = \mathbb{1}_{z \geq \theta}$ | $f(z) = \sigma(z), \tanh(z), \text{ReLU}(z)$ |
| Aprendizaje | Manual (umbral fijo) | Aprende pesos y bias con gradient descent |
Donde:
| Función | Fórmula | Uso común |
|---|---|---|
| Sigmoide | $\sigma(z) = \frac{1}{1 + e^{-z}}$ | Problemas binarios, salida [0,1] |
| Tanh | $\tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}}$ | Salida en [-1,1] |
| ReLU | $\max(0, z)$ | Muy usada en capas ocultas |
Es una operación matemática que se aplica a la salida ponderada de cada neurona, es decir, después de calcular:
$$ z = \sum_{i=1}^{n} w_i x_i + b $$la salida de la neurona es:
$$ a = f(z) $$Donde $f(\cdot)$ es la función de activación.
Si cada capa fuera puramente lineal:
$$ a = w_2(w_1 x + b_1) + b_2 $$entonces toda la red sería una sola transformación lineal. No podría aprender relaciones complejas no lineales.
| Propiedad | Descripción |
|---|---|
| No linealidad | Permite que la red aprenda relaciones complejas y no triviales |
| Separabilidad | Ayuda a distinguir clases en espacios de entrada complejos |
| Capacidad universal | Una red con activaciones no lineales puede aproximar cualquier función continua (teorema universal) |
| Diferenciabilidad | Permite usar descenso por gradiente para ajustar los pesos durante el aprendizaje |
| Propiedades | Valor |
|---|---|
| Rango | (0, 1) |
| Ventaja | Interpretable como probabilidad |
| Problemas | Gradiente se aplana → desvanecimiento del gradiente |
| Propiedades | Valor |
|---|---|
| Rango | (-1, 1) |
| Ventaja | Centrado en cero |
| Problemas | Mismo que sigmoide |
| Propiedades | Valor |
|---|---|
| Rango | [0, ∞) |
| Ventaja | Computacionalmente eficiente, evita saturación |
| Problemas | Muerte de neuronas si la entrada es negativa por mucho tiempo |
| Capa | Activación típica |
|---|---|
| Ocultas (interiores) | ReLU, Leaky ReLU, tanh |
| Salida binaria | Sigmoide |
| Salida multiclase | Softmax |
| Salida regresiva | Lineal |


Donde $\Phi(z)$ es la función de distribución acumulada de la normal estándar:
$$ \Phi(z) = \frac{1}{2} \left(1 + \text{erf}\left( \frac{z}{\sqrt{2}} \right)\right) $$| Propiedad | Valor |
|---|---|
| Rango | Continuo (similar a ReLU para positivos, pero más suave) |
| Diferenciable | Sí (suave y continuo) |
| Ventaja | Combina los beneficios de ReLU y Sigmoid (activación y suavidad) |
| Uso actual | Muy usada en Transformers, BERT, GPT y redes modernas de NLP |
| Función | Rango | Ventaja clave | Problema |
|---|---|---|---|
| Sigmoid | (0, 1) | Interpretable, salida binaria | Gradiente pequeño |
| Tanh | (-1, 1) | Centrado | Gradiente pequeño |
| ReLU | [0, ∞) | Rápida, simple, no saturación | Muertes (z < 0) |
| LeakyReLU | [≈−∞, ∞) | Evita muertes, simple | Algo arbitraria |
| Softmax | (0, 1), suma 1 | Probabilidades multicategoría | No para capas ocultas |
| GELU | Real line | Activación suave y adaptativa, bien fundamentada | Costo computacional (leve) |
Las funciones de activación dan a las redes neuronales su poder expresivo. Permiten que las capas trabajen en conjunto para aprender patrones complejos, no lineales y altamente abstractos, que serían imposibles de representar con funciones lineales.
Una regresión lineal simple se puede interpretar como un caso particular de una red neuronal de una sola capa con una estructura mínima: una neurona de entrada, un sesgo (bias) y una neurona de salida sin función de activación (o con activación lineal).
Esto es exactamente la ecuación de la regresión lineal simple.
Para ajustar los parámetros $w$ y $b$, se utiliza típicamente el error cuadrático medio (MSE) como función de pérdida:
$$ \mathcal{L}(w, b) = \frac{1}{n} \sum_{i=1}^{n} \left( y_i - (w x_i + b) \right)^2 $$Donde:
Para encontrar los valores óptimos de $w$ y $b$, se utiliza el descenso de gradiente:
En cada iteración:
Donde $\eta$ es la tasa de aprendizaje.
Una regresión lineal simple es funcionalmente equivalente a una red neuronal con una sola capa lineal, sin activación, que aprende a ajustar sus parámetros a partir de un conjunto de datos mediante descenso por gradiente.

ŷ (lineal)Este modelo calcula:
$$ \hat{y} = w_1 x_1 + b $$Es decir, predice una salida continua a partir de una sola variable, exactamente como lo hace la regresión lineal simple.
Las redes neuronales funcionan mejor cuando las entradas y salidas están en rangos similares. Si no se escalan:
| Problema | Consecuencia |
|---|---|
| Diferentes rangos en variables | Algunas dominan el aprendizaje, otras se ignoran |
| Gradientes muy pequeños o grandes | Entrenamiento inestable o muy lento |
| Saturación de activaciones | Funciones como Sigmoid/Tanh pueden quedar sin gradiente |
| Explosión o desaparición de gradientes | No aprendizaje en capas profundas |
| Método | Fórmula | Rango típico | Uso común en | ||
|---|---|---|---|---|---|
| MinMaxScaler | $x' = \frac{x - \min(x)}{\max(x) - \min(x)}$ | $[0, 1]$ | Imágenes, sensores | ||
| StandardScaler | $x' = \frac{x - \mu}{\sigma}$ | media 0, var 1 | Datos normales | ||
| RobustScaler | Basado en mediana y IQR | insensible a outliers | Datos con outliers | ||
| MaxAbsScaler | $x' = \dfrac{x}{\max\left( | x | \right)}$ | $[-1, 1]$ | Datos dispersos, centrados en 0 |
| Log Transform | $x' = \log(x + 1)$ | Reduce escala | Datos positivos sesgados |
inverse_transform)| Situación | Escalado recomendado |
|---|---|
| Datos densos y en diferentes unidades | StandardScaler |
| Imagen en píxeles (0–255) | MinMaxScaler |
| Datos económicos con outliers | RobustScaler |
| Todo positivo, exponencial | LogTransform + MinMax |
import numpy as np
import matplotlib.pyplot as plt
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
import plotly.graph_objects as go
# Original data: Altura (pulgadas) y Peso (libras)
X_raw = np.array([[63], [64], [66], [69], [69], [71], [71], [72], [73], [75]])
y_raw = np.array([[127], [121], [142], [157], [162], [156], [169], [165], [181], [208]])
# Escalado min-max
X_min, X_max = X_raw.min(), X_raw.max()
y_min, y_max = y_raw.min(), y_raw.max()
X = (X_raw - X_min) / (X_max - X_min)
y = (y_raw - y_min) / (y_max - y_min)
# Inicialización de pesos y sesgo para la neurona
np.random.seed(0)
w = np.random.randn(1, 1)
b = np.zeros((1,))
# Función de activación lineal y pérdida
def activation(z):
return z
def mse(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
# Descenso de gradiente
lr = 0.05
n_epochs = 1000
for epoch in range(n_epochs):
z = X @ w + b
y_pred = activation(z)
error = y_pred - y
loss = mse(y, y_pred)
dw = X.T @ error / len(X)
db = np.mean(error)
w -= lr * dw
b -= lr * db
# Predicción y desescalado
y_pred_scaled = X @ w + b
y_pred_nn = y_pred_scaled * (y_max - y_min) + y_min
# Regresión lineal con sklearn
model = LinearRegression()
model.fit(X_raw, y_raw)
y_pred_lr = model.predict(X_raw)
# Cálculo de pesos en escala original para la neurona
w_original = (y_max - y_min) / (X_max - X_min) * w[0, 0]
b_original = (y_max - y_min) * b[0] + y_min - w_original * X_min
# MSE
mse_nn = mean_squared_error(y_raw, y_pred_nn)
mse_lr = mean_squared_error(y_raw, y_pred_lr)
# ============================
# 📊 PLOTLY INTERACTIVO
# ============================
# Ordenar para curvas suaves
sorted_idx = np.argsort(X_raw[:, 0])
X_sorted = X_raw[sorted_idx].flatten()
y_sorted = y_raw[sorted_idx].flatten()
y_pred_nn_sorted = y_pred_nn[sorted_idx].flatten()
y_pred_lr_sorted = y_pred_lr[sorted_idx].flatten()
# Crear gráfico
fig = go.Figure()
# Datos reales
fig.add_trace(go.Scatter(x=X_sorted, y=y_sorted, mode='markers',
marker=dict(color='black', size=8),
name='Datos reales'))
# Predicción neurona
fig.add_trace(go.Scatter(x=X_sorted, y=y_pred_nn_sorted, mode='lines+markers',
line=dict(color='red', dash='dash'),
name='Neurona (descenso gradiente)'))
# Predicción regresión lineal
fig.add_trace(go.Scatter(x=X_sorted, y=y_pred_lr_sorted, mode='lines+markers',
line=dict(color='blue', dash='dot'),
name='Regresión lineal (Sklearn)'))
# Líneas de error (residuos) neurona
for i in range(len(X_sorted)):
fig.add_trace(go.Scatter(
x=[X_sorted[i], X_sorted[i]],
y=[y_sorted[i], y_pred_nn_sorted[i]],
mode='lines',
line=dict(color='red', width=2),
showlegend=False
))
# Líneas de error (residuos) regresión lineal
for i in range(len(X_sorted)):
fig.add_trace(go.Scatter(
x=[X_sorted[i], X_sorted[i]],
y=[y_sorted[i], y_pred_lr_sorted[i]],
mode='lines',
line=dict(color='blue', width=2),
showlegend=False
))
# Layout
fig.update_layout(
title="Altura vs Peso: Predicciones y Errores con Plotly",
xaxis_title="Altura (pulgadas)",
yaxis_title="Peso (libras)",
height=500,
legend=dict(x=0.01, y=0.99),
margin=dict(l=20, r=20, t=40, b=20)
)
fig.show()
# ============================
# 🧾 Métricas y pesos
# ============================
print({
"NN_weight_rescaled": round(float(w_original), 4),
"NN_bias_rescaled": round(float(b_original), 4),
"LinearRegression_coef": round(float(model.coef_[0, 0]), 4),
"LinearRegression_intercept": round(float(model.intercept_[0]), 4),
"MSE_NeuralNetwork": round(mse_nn, 2),
"MSE_LinearRegression": round(mse_lr, 2)
})
{'NN_weight_rescaled': 6.261, 'NN_bias_rescaled': -275.1497, 'LinearRegression_coef': 6.1376, 'LinearRegression_intercept': -266.5344, 'MSE_NeuralNetwork': 59.95, 'MSE_LinearRegression': 59.74}
📌 Esta es exactamente la fórmula de la regresión lineal múltiple.
Para entrenar la red, se usa una función de pérdida que mida la diferencia entre las predicciones $\hat{y}_i$ y los valores reales $y_i$:
$$ \mathcal{L}(\mathbf{w}, b) = \frac{1}{n} \sum_{i=1}^{n} \left( y_i - (\mathbf{w}^\top \mathbf{x}_i + b) \right)^2 $$En cada iteración del entrenamiento:
donde $\eta$ es la tasa de aprendizaje.

ŷ (lineal)Este modelo calcula:
$$ \hat{y} = w_1 x_1 + w_2 x_2 + w_3 x_3 + b $$Lo que corresponde a la regresión lineal múltiple, usada para predecir un valor continuo a partir de varias variables independientes.
Ambas redes (regresión simple y múltiple) son completamente conectadas, sin capas ocultas ni funciones de activación no lineales, reflejando el comportamiento clásico de los modelos lineales. ¿Quieres que incluya estas explicaciones en formato Markdown o LaTeX para tus notas o presentaciones?
Una regresión lineal múltiple es funcionalmente equivalente a una red neuronal de una sola capa completamente conectada, sin función de activación, que aprende a ajustar pesos y sesgo usando descenso por gradiente.
# Complete code for training a neural network and comparing to LinearRegression
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error
# Load the dataset
df = pd.read_csv("https://raw.githubusercontent.com/marsgr6/r-scripts/refs/heads/master/data/viz_data/multiple_linear_regression_dataset.csv")
# Extract features and target
X_raw = df[['age', 'experience']].values
y_raw = df[['income']].values
# Min-max scaling
scaler_X = MinMaxScaler()
scaler_y = MinMaxScaler()
X = scaler_X.fit_transform(X_raw)
y = scaler_y.fit_transform(y_raw)
# Initialize weights and bias
np.random.seed(0)
w = np.random.randn(2, 1)
b = np.zeros((1,))
# Activation and loss
def activation(z):
return z
def mse(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
# Training via gradient descent
lr = 0.1
n_epochs = 1000
for epoch in range(n_epochs):
z = X @ w + b
y_pred = activation(z)
error = y_pred - y
loss = mse(y, y_pred)
dw = X.T @ error / len(X)
db = np.mean(error)
w -= lr * dw
b -= lr * db
# Predict and inverse transform
y_pred_scaled = X @ w + b
y_pred_nn = scaler_y.inverse_transform(y_pred_scaled)
# Sklearn model
lr_model = LinearRegression()
lr_model.fit(X_raw, y_raw)
y_pred_sklearn = lr_model.predict(X_raw)
# Plot predictions
plt.figure(figsize=(15, 5))
plt.subplot(1,2,1)
plt.plot(y_raw, label="Actual Income", marker='o')
plt.plot(y_pred_nn, label="Neural Network Prediction", linestyle='--', marker='x')
plt.plot(y_pred_sklearn, label="Sklearn Linear Regression", linestyle='-.', marker='s')
plt.xlabel("Sample Index")
plt.ylabel("Income")
plt.title("Comparison of Neural Network vs Sklearn Linear Regression")
plt.legend()
plt.grid(True)
plt.subplot(1,2,2)
plt.scatter(y_raw, y_pred_nn, marker='o', label="Neural Network Prediction")
plt.scatter(y_raw, y_pred_sklearn, marker='+', label="Linear Regression Prediction")
plt.xlabel("Observed Income")
plt.ylabel("Predicted Income")
plt.title("Comparison of Neural Network vs Sklearn Linear Regression")
# Plot the line x=y
x_line = np.linspace(min(y_raw), max(y_raw), 100)
y_line = x_line
plt.plot(x_line, y_line, label='x=y', color='red', linestyle='--')
plt.legend()
plt.grid(True)
plt.show()
# Compute MSE
mse_nn = mean_squared_error(y_raw, y_pred_nn)
mse_sklearn = mean_squared_error(y_raw, y_pred_sklearn)
# Convert NN weights to original scale
X_min = scaler_X.data_min_
X_max = scaler_X.data_max_
y_min = scaler_y.data_min_[0]
y_max = scaler_y.data_max_[0]
s_x = X_max - X_min
s_y = y_max - y_min
w_original = (s_y / s_x).reshape(-1, 1) * w
bias_original = (
s_y * b[0]
+ y_min
- np.sum((w_original.flatten() * X_min))
)
{
"NN_weights_rescaled": w_original.flatten(),
"NN_bias_rescaled": bias_original,
"MSE_NeuralNetwork": mse_nn,
"MSE_LinearRegression": mse_sklearn,
"LinearRegression_weights": lr_model.coef_.flatten(),
"LinearRegression_intercept": lr_model.intercept_[0]
}
{'NN_weights_rescaled': array([ -23.02758776, 1969.77549655]),
'NN_bias_rescaled': 29428.431026235587,
'MSE_NeuralNetwork': 1978276.6008504115,
'MSE_LinearRegression': 1533633.9787141785,
'LinearRegression_weights': array([ -99.19535546, 2162.40419192]),
'LinearRegression_intercept': 31261.68985410128}
# Reload the uploaded dataset
df = pd.read_csv("https://raw.githubusercontent.com/marsgr6/r-scripts/refs/heads/master/data/viz_data/multiple_linear_regression_dataset.csv")
# Extract features and target
X_raw = df[['age', 'experience']].values
y_raw = df[['income']].values
# Min-max scaling
scaler_X = MinMaxScaler()
scaler_y = MinMaxScaler()
X = scaler_X.fit_transform(X_raw)
y = scaler_y.fit_transform(y_raw)
# Initialize weights and bias for the neural network
np.random.seed(0)
w = np.random.randn(2, 1)
b = np.zeros((1,))
# Define activation and loss
def activation(z):
return z
def mse(y_true, y_pred):
return np.mean((y_true - y_pred) ** 2)
# Train neural network using gradient descent
lr = 0.1
n_epochs = 1000
for epoch in range(n_epochs):
z = X @ w + b
y_pred = activation(z)
error = y_pred - y
loss = mse(y, y_pred)
dw = X.T @ error / len(X)
db = np.mean(error)
w -= lr * dw
b -= lr * db
# Predict and inverse scale
y_pred_scaled = X @ w + b
y_pred_nn = scaler_y.inverse_transform(y_pred_scaled)
# Train scikit-learn linear regression model
lr_model = LinearRegression()
lr_model.fit(X_raw, y_raw)
y_pred_sklearn = lr_model.predict(X_raw)
# Prepare meshgrid for 3D surface plot
x1_range = np.linspace(X_raw[:, 0].min(), X_raw[:, 0].max(), 50)
x2_range = np.linspace(X_raw[:, 1].min(), X_raw[:, 1].max(), 50)
x1_grid, x2_grid = np.meshgrid(x1_range, x2_range)
x_grid_flat = np.column_stack([x1_grid.ravel(), x2_grid.ravel()])
# Predict using neural network on the grid
X_grid_scaled = scaler_X.transform(x_grid_flat)
y_pred_nn_grid_scaled = X_grid_scaled @ w + b
y_pred_nn_grid = scaler_y.inverse_transform(y_pred_nn_grid_scaled)
Z_nn = y_pred_nn_grid.reshape(x1_grid.shape)
# Predict using sklearn model on the grid
y_pred_lr_grid = lr_model.predict(x_grid_flat)
Z_lr = y_pred_lr_grid.reshape(x1_grid.shape)
# Plotting both surfaces
fig = plt.figure(figsize=(14, 6))
# Plot both 3D surfaces in a single plot for direct comparison
import plotly.graph_objects as go
# Predict income on raw data using both models
y_pred_nn_pts = scaler_y.inverse_transform((X @ w + b))
y_pred_lr_pts = lr_model.predict(X_raw)
# Create plotly figure
fig = go.Figure()
# Neural network surface
fig.add_trace(go.Surface(
z=Z_nn, x=x1_grid, y=x2_grid,
colorscale='Reds', opacity=0.5,
name='Neural Network', showscale=False
))
# Linear regression surface
fig.add_trace(go.Surface(
z=Z_lr, x=x1_grid, y=x2_grid,
colorscale='Blues', opacity=0.5,
name='Linear Regression', showscale=False
))
# Actual data points
fig.add_trace(go.Scatter3d(
x=X_raw[:, 0], y=X_raw[:, 1], z=y_raw.flatten(),
mode='markers',
marker=dict(size=4, color='black'),
name='Actual Data'
))
# Residual lines (Neural Network)
for i in range(len(X_raw)):
fig.add_trace(go.Scatter3d(
x=[X_raw[i, 0], X_raw[i, 0]],
y=[X_raw[i, 1], X_raw[i, 1]],
z=[y_raw[i, 0], y_pred_nn_pts[i, 0]],
mode='lines',
line=dict(color='red', width=2),
showlegend=False
))
# Residual lines (Linear Regression)
for i in range(len(X_raw)):
fig.add_trace(go.Scatter3d(
x=[X_raw[i, 0], X_raw[i, 0]],
y=[X_raw[i, 1], X_raw[i, 1]],
z=[y_raw[i, 0], y_pred_lr_pts[i, 0]],
mode='lines',
line=dict(color='blue', width=2),
showlegend=False
))
# Layout
fig.update_layout(
title='3D Surfaces with Data Points and Residuals',
scene=dict(
xaxis_title='Age',
yaxis_title='Experience',
zaxis_title='Income'
),
margin=dict(l=0, r=0, b=0, t=40)
)
fig.show()
<Figure size 1400x600 with 0 Axes>
La regresión logística binaria es equivalente a una red neuronal de una sola capa (sin capas ocultas) que contiene:
Este es el equivalente neuronal del modelo utilizado para estimar la probabilidad de un resultado binario.
Dado un vector de entrada $\mathbf{x} \in \mathbb{R}^d$, el modelo calcula:
$$ z = \mathbf{w}^\top \mathbf{x} + b $$$$ \hat{y} = \sigma(z) = \frac{1}{1 + e^{-z}} $$Donde:
A continuación se muestra un diagrama conceptual que representa la regresión logística binaria como una red neuronal:

ŷ (sigmoid) que aplica la activación sigmoideEsta arquitectura calcula:
$$ \hat{y} = \sigma(w_1 x_1 + w_2 x_2 + w_3 x_3 + b) $$Esto refleja el modelo de regresión logística binaria, representado como una red neuronal de una sola capa.
| Componente | Regresión Logística | Equivalente en Red Neuronal |
|---|---|---|
| Tipo de modelo | Clasificador lineal binario | Red neuronal de una sola capa con sigmoide |
| Activación | Sigmoide | Sigmoide |
| Salida | Probabilidad de clase 1 | Igual |
| Pérdida | Entropía cruzada binaria | Igual |
Este ejemplo muestra cómo incluso las redes neuronales más simples están fuertemente relacionadas con los modelos estadísticos clásicos.
import numpy as np
import plotly.graph_objects as go
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import MinMaxScaler
from sklearn.metrics import accuracy_score, confusion_matrix, ConfusionMatrixDisplay
# 1. Generate data
X_uni, y_uni = make_classification(n_samples=100, n_features=1, n_informative=1,
n_redundant=0, n_clusters_per_class=1, flip_y=0.03, random_state=42)
# 2. Scale input
scaler_uni = MinMaxScaler()
X_uni_scaled = scaler_uni.fit_transform(X_uni)
# 3. Sigmoid activation
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 4. Initialize neural network
np.random.seed(0)
w_uni = np.random.randn(1, 1)
b_uni = np.zeros((1,))
lr = 0.25
n_epochs = 1000
# 5. Train with Binary Cross-Entropy
for epoch in range(n_epochs):
z = X_uni_scaled @ w_uni + b_uni
y_prob_uni_nn = sigmoid(z)
eps = 1e-15
y_clipped = np.clip(y_prob_uni_nn, eps, 1 - eps)
loss = -np.mean(y_uni.reshape(-1, 1) * np.log(y_clipped) +
(1 - y_uni).reshape(-1, 1) * np.log(1 - y_clipped))
error = y_prob_uni_nn - y_uni.reshape(-1, 1)
dw = X_uni_scaled.T @ error / len(X_uni_scaled)
db = np.mean(error)
w_uni -= lr * dw
b_uni -= lr * db
# 6. Predict with neural network
y_pred_uni_nn = (y_prob_uni_nn >= 0.5).astype(int)
# 7. Train logistic regression (sklearn)
model_uni = LogisticRegression()
model_uni.fit(X_uni, y_uni)
y_prob_lr = model_uni.predict_proba(X_uni)[:, 1]
y_pred_lr = model_uni.predict(X_uni)
# 8. Accuracy
acc_nn = accuracy_score(y_uni, y_pred_uni_nn)
acc_lr = accuracy_score(y_uni, y_pred_lr)
# 9. Sorted for plotting
sorted_idx = np.argsort(X_uni[:, 0])
X_sorted = X_uni[sorted_idx].flatten()
X_scaled_sorted = X_uni_scaled[sorted_idx]
y_prob_nn_sorted = sigmoid(X_scaled_sorted @ w_uni + b_uni).flatten()
y_prob_lr_sorted = y_prob_lr[sorted_idx]
y_pred_cls_sorted = y_pred_lr[sorted_idx]
# 10. Create Plotly figure
fig = go.Figure()
# True class data points
fig.add_trace(go.Scatter(
x=X_uni[y_uni == 0].flatten(), y=y_uni[y_uni == 0],
mode='markers', marker=dict(color='blue', size=8),
name='Clase 0 (real)'
))
fig.add_trace(go.Scatter(
x=X_uni[y_uni == 1].flatten(), y=y_uni[y_uni == 1],
mode='markers', marker=dict(color='green', size=8),
name='Clase 1 (real)'
))
# Prediction curve: sklearn logistic regression
fig.add_trace(go.Scatter(
x=X_sorted, y=y_prob_lr_sorted,
mode='lines', line=dict(color='orange', dash='dot'),
name='Logistic Regression (sklearn)'
))
# Prediction curve: neural network with BCE
fig.add_trace(go.Scatter(
x=X_sorted, y=y_prob_nn_sorted,
mode='lines', line=dict(color='red'),
name='Neurona (BCE)'
))
# Modeled points (predicted class)
fig.add_trace(go.Scatter(
x=X_sorted, y=y_prob_lr_sorted,
mode='markers',
marker=dict(
color=['green' if c == 1 else 'blue' for c in y_pred_cls_sorted],
size=7,
symbol='cross'
),
name='Puntos modelados (predicción)'
))
# Layout
fig.update_layout(
title="Clasificación binaria univariada<br>Neurona (BCE) vs Logistic Regression",
xaxis_title="X",
yaxis_title="Probabilidad estimada",
height=500,
legend=dict(x=0.01, y=0.99),
margin=dict(l=20, r=20, t=40, b=20)
)
fig.show()
# 11. Print accuracy and confusion matrices
print("📊 Accuracy:")
print(f"Neural Network (BCE): {acc_nn:.2f}")
print(f"Logistic Regression: {acc_lr:.2f}")
# 9. Plot confusion matrices
cm_multi_nn = confusion_matrix(y_uni, y_pred_uni_nn)
cm_multi_lr = confusion_matrix(y_uni, y_pred_lr)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
ConfusionMatrixDisplay(cm_multi_nn).plot(ax=axes[0], cmap='Reds')
axes[0].set_title("Neural Network - Univariate")
ConfusionMatrixDisplay(cm_multi_lr).plot(ax=axes[1], cmap='Blues')
axes[1].set_title("Logistic Regression - Univariate")
# Print the coefficients
print("Logreg Coefficients:", model_uni.coef_)
print("Logreg Intercept:", model_uni.intercept_)
📊 Accuracy: Neural Network (BCE): 0.98 Logistic Regression: 0.98 Logreg Coefficients: [[3.24293979]] Logreg Intercept: [0.32284014]
Asumiendo un modelo simple de regresión logística:
$$ P(y = 1 \mid x) = \frac{1}{1 + e^{-(\beta_0 + \beta_1 x)}} $$Si tomamos $\beta_1 = 1$ para simplificar, el modelo se convierte en:
$$ P(y = 1 \mid x) = \frac{1}{1 + e^{-(0.3228 + x)}} $$La probabilidad es 0.5 cuando el exponente es cero, es decir:
$$ 0.3228 + x = 0 \quad \Rightarrow \quad x = -0.3228 $$En $x = 0$, la probabilidad estimada es:
$$ P(y = 1 \mid x = 0) = \frac{1}{1 + e^{-0.3228}} \approx 0.58 $$
# Full pipeline: Multivariate Logistic Regression (Neural Net vs Sklearn) with Confusion Matrix
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix, ConfusionMatrixDisplay
from sklearn.preprocessing import MinMaxScaler
# 1. Generate multivariate classification dataset
X_multi, y_multi = make_classification(n_samples=200, n_features=3, n_informative=3,
n_redundant=0, n_clusters_per_class=1, flip_y=0.03, random_state=42)
# 2. Scale features
scaler_multi = MinMaxScaler()
X_multi_scaled = scaler_multi.fit_transform(X_multi)
# 3. Sigmoid activation
def sigmoid(z):
return 1 / (1 + np.exp(-z))
# 4. Initialize neural network weights
np.random.seed(0)
w_multi = np.random.randn(3, 1)
b_multi = np.zeros((1,))
lr = 0.1
n_epochs = 1000
# 5. Train neural network
for epoch in range(n_epochs):
z = X_multi_scaled @ w_multi + b_multi
y_prob_multi_nn = sigmoid(z)
error = y_prob_multi_nn - y_multi.reshape(-1, 1)
dw = X_multi_scaled.T @ error / len(X_multi_scaled)
db = np.mean(error)
w_multi -= lr * dw
b_multi -= lr * db
# 6. Neural network prediction
y_pred_multi_nn = (y_prob_multi_nn >= 0.5).astype(int)
# 7. Sklearn logistic regression
model_multi = LogisticRegression()
model_multi.fit(X_multi, y_multi)
y_pred_lr_multi = model_multi.predict(X_multi)
# 8. Accuracy
acc_nn_multi = accuracy_score(y_multi, y_pred_multi_nn)
acc_lr_multi = accuracy_score(y_multi, y_pred_lr_multi)
# 9. Plot confusion matrices
cm_multi_nn = confusion_matrix(y_multi, y_pred_multi_nn)
cm_multi_lr = confusion_matrix(y_multi, y_pred_lr_multi)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
ConfusionMatrixDisplay(cm_multi_nn).plot(ax=axes[0], cmap='Reds')
axes[0].set_title("Neural Network - Multivariate")
ConfusionMatrixDisplay(cm_multi_lr).plot(ax=axes[1], cmap='Blues')
axes[1].set_title("Logistic Regression - Multivariate")
plt.tight_layout()
plt.show()
# 10. Output metrics
{
"Multivariate Neural Network Accuracy": acc_nn_multi,
"Multivariate Logistic Regression Accuracy": acc_lr_multi
}
{'Multivariate Neural Network Accuracy': 0.9,
'Multivariate Logistic Regression Accuracy': 0.925}
import plotly.graph_objects as go
# Predict probabilities and classes
y_pred_lr_cls = model_uni.predict(X_uni)
y_prob_lr = model_uni.predict_proba(X_uni)[:, 1]
# Sort for clean sigmoid curve
sorted_idx = np.argsort(X_uni[:, 0])
X_sorted = X_uni[sorted_idx].flatten()
y_prob_lr_sorted = y_prob_lr[sorted_idx]
y_pred_cls_sorted = y_pred_lr_cls[sorted_idx]
# Plotly interactive figure
fig = go.Figure()
# Original data points by class
fig.add_trace(go.Scatter(
x=X_uni[y_uni == 0].flatten(), y=y_uni[y_uni == 0],
mode='markers',
marker=dict(color='blue', size=8),
name='Clase 0 (real)'
))
fig.add_trace(go.Scatter(
x=X_uni[y_uni == 1].flatten(), y=y_uni[y_uni == 1],
mode='markers',
marker=dict(color='green', size=8),
name='Clase 1 (real)'
))
# Logistic regression sigmoid curve
fig.add_trace(go.Scatter(
x=X_sorted, y=y_prob_lr_sorted,
mode='lines',
line=dict(color='orange', dash='dash'),
name='Curva logística (sklearn)'
))
# Modeled predicted points colored by predicted class
fig.add_trace(go.Scatter(
x=X_sorted,
y=y_prob_lr_sorted,
mode='markers',
marker=dict(
color=['green' if c == 1 else 'blue' for c in y_pred_cls_sorted],
size=10,
symbol='cross'
),
name='Puntos modelados (predicción)'
))
fig.update_layout(
title="Logistic Regression: curva y clases predichas",
xaxis_title="X",
yaxis_title="Probabilidad",
height=500,
legend=dict(y=0.01, x=0.69),
margin=dict(l=20, r=20, t=40, b=20)
)
fig.show()
La regresión logística multinomial puede visualizarse como una red neuronal de una sola capa (sin capas ocultas), donde:
Para un vector de características $\mathbf{x} \in \mathbb{R}^d$ y $K$ clases, el modelo calcula:
$$ z_k = \mathbf{w}_k^\top \mathbf{x} + b_k \quad \text{para } k = 1, \dots, K $$$$ P(y = k \mid \mathbf{x}) = \frac{e^{z_k}}{\sum_{j=1}^K e^{z_j}} $$Esto es exactamente lo que hace una red neuronal con:
A continuación se muestra una visualización de esta estructura con 3 características de entrada y 4 clases de salida:

class_1, class_2, class_3, etc., cada una aplica la activación softmax de manera conjuntaEsta arquitectura calcula para cada clase $k$:
$$ z_k = w_{k1} x_1 + w_{k2} x_2 + w_{k3} x_3 + b_k $$y luego transforma estos valores con softmax:
$$ P(y = k \mid \mathbf{x}) = \frac{e^{z_k}}{\sum_{j=1}^K e^{z_j}} $$Esto refleja el modelo de regresión logística multinomial, expresado como una red neuronal de una sola capa, sin capas ocultas.
import networkx as nx
import matplotlib.pyplot as plt
from ipywidgets import interact, Dropdown, IntSlider, Checkbox, fixed
import matplotlib.patches as mpatches
# Función principal corregida
def draw_custom_nn(input_size, use_bias, hidden_layers, hidden_neurons_per_layer, output_size, architecture='mlp'):
G = nx.DiGraph()
pos = {}
layer_colors = {
'input': '#8ecae6',
'hidden': '#219ebc',
'output': '#ffb703',
'bias': '#adb5bd'
}
edge_labels = {}
plt.figure(figsize=(12, 6))
ax = plt.gca()
ax.set_title("Neural Network Architecture", fontsize=14)
# Input layer
for i in range(input_size):
node = f"x{i+1}"
G.add_node(node, layer='input')
pos[node] = (0, input_size - i - 1)
if use_bias:
G.add_node("bias_input", layer='bias')
pos["bias_input"] = (0, -1)
if architecture == 'mlp':
# Hidden layers and biases
for l in range(hidden_layers):
layer_offset = 1 + l
for j in range(hidden_neurons_per_layer):
h_node = f"h{l+1}_{j+1}"
G.add_node(h_node, layer='hidden')
pos[h_node] = (layer_offset, hidden_neurons_per_layer / 2 - j)
if use_bias and l < hidden_layers - 1:
bias_node = f"bias_hidden{l+1}"
G.add_node(bias_node, layer='bias')
pos[bias_node] = (layer_offset + 0.25, -1*hidden_neurons_per_layer / 2)
if use_bias and hidden_layers > 0:
G.add_node("bias_output", layer='bias')
pos["bias_output"] = (hidden_layers + 0.25, -1*hidden_neurons_per_layer / 2)
# Conexiones entre capas ocultas
for l in range(hidden_layers):
prev_nodes = [f"x{i+1}" for i in range(input_size)] if l == 0 else [f"h{l}_{i+1}" for i in range(hidden_neurons_per_layer)]
curr_nodes = [f"h{l+1}_{j+1}" for j in range(hidden_neurons_per_layer)]
for u in prev_nodes:
for v in curr_nodes:
G.add_edge(u, v)
if use_bias:
bias_source = "bias_input" if l == 0 else f"bias_hidden{l}"
for v in curr_nodes:
G.add_edge(bias_source, v)
# Capa de salida
for k in range(output_size):
o_node = f"ŷ{k+1}"
G.add_node(o_node, layer='output')
pos[o_node] = (1 + hidden_layers, output_size / 2 - k)
# Conexiones a la salida
if hidden_layers > 0:
last_hidden = [f"h{hidden_layers}_{j+1}" for j in range(hidden_neurons_per_layer)]
for u in last_hidden:
for k in range(output_size):
G.add_edge(u, f"ŷ{k+1}")
if use_bias:
for k in range(output_size):
G.add_edge("bias_output", f"ŷ{k+1}")
else:
for i in range(input_size):
for k in range(output_size):
G.add_edge(f"x{i+1}", f"ŷ{k+1}")
if use_bias:
for k in range(output_size):
G.add_edge("bias_input", f"ŷ{k+1}")
elif architecture == 'multinomial_logistic_regression':
for k in range(output_size):
o_node = f"class_{k+1}"
G.add_node(o_node, layer='output')
pos[o_node] = (1, output_size - k - 1)
for i in range(input_size):
for k in range(output_size):
G.add_edge(f"x{i+1}", f"class_{k+1}")
#edge_labels[(f"x{i+1}", f"class_{k+1}")] = f"a{i+1}"
if use_bias:
for k in range(output_size):
G.add_edge("bias_input", f"class_{k+1}")
#edge_labels[("bias_input", f"class_{k+1}")] = "b"
else: # regresión lineal
G.add_node("ŷ", layer='output')
pos["ŷ"] = (1, 0)
for i in range(input_size):
G.add_edge(f"x{i+1}", "ŷ")
edge_labels[(f"x{i+1}", "ŷ")] = f"a{i+1}"
if use_bias:
G.add_edge("bias_input", "ŷ")
edge_labels[("bias_input", "ŷ")] = "b"
# Dibujar red
node_colors = [layer_colors[G.nodes[n]['layer']] for n in G.nodes]
nx.draw(G, pos, with_labels=True, arrows=True, node_size=1500,
node_color=node_colors, font_size=10, edge_color='gray')
# Draw edge labels only for regression cases
if architecture in ['regression', 'multinomial_logistic_regression']:
nx.draw_networkx_edge_labels(G, pos, edge_labels=edge_labels, font_color='red')
patches = [mpatches.Patch(color=color, label=label.capitalize()) for label, color in layer_colors.items()]
plt.legend(handles=patches, loc='center left', bbox_to_anchor=(1, 0.5))
plt.axis('off')
plt.tight_layout()
plt.show()
# Interfaz interactiva
def interactive_nn(model_type):
configs = {
'simple_linear_regression': {
'architecture': 'regression',
'input_size': {'min': 1, 'max': 1, 'value': 1},
'hidden_layers': {'min': 0, 'max': 0, 'value': 0},
'hidden_neurons_per_layer': {'min': 0, 'max': 0, 'value': 0},
'output_size': {'min': 1, 'max': 1, 'value': 1}
},
'multiple_linear_regression': {
'architecture': 'regression',
'input_size': {'min': 2, 'max': 10, 'value': 3},
'hidden_layers': {'min': 0, 'max': 0, 'value': 0},
'hidden_neurons_per_layer': {'min': 0, 'max': 0, 'value': 0},
'output_size': {'min': 1, 'max': 1, 'value': 1}
},
'multinomial_logistic_regression': {
'architecture': 'multinomial_logistic_regression',
'input_size': {'min': 1, 'max': 10, 'value': 3},
'hidden_layers': {'min': 0, 'max': 0, 'value': 0},
'hidden_neurons_per_layer': {'min': 0, 'max': 0, 'value': 0},
'output_size': {'min': 2, 'max': 10, 'value': 4}
},
'mlp': {
'architecture': 'mlp',
'input_size': {'min': 2, 'max': 10, 'value': 3},
'hidden_layers': {'min': 0, 'max': 3, 'value': 1},
'hidden_neurons_per_layer': {'min': 1, 'max': 10, 'value': 4},
'output_size': {'min': 1, 'max': 10, 'value': 2}
}
}
config = configs[model_type]
interact(draw_custom_nn,
input_size=IntSlider(min=config['input_size']['min'],
max=config['input_size']['max'],
step=1,
value=config['input_size']['value'],
description='Inputs:'),
use_bias=Checkbox(value=True, description='Include bias'),
hidden_layers=IntSlider(min=config['hidden_layers']['min'],
max=config['hidden_layers']['max'],
step=1,
value=config['hidden_layers']['value'],
description='Hidden layers:'),
hidden_neurons_per_layer=IntSlider(min=config['hidden_neurons_per_layer']['min'],
max=config['hidden_neurons_per_layer']['max'],
step=1,
value=config['hidden_neurons_per_layer']['value'],
description='Neurons per layer:'),
output_size=IntSlider(min=config['output_size']['min'],
max=config['output_size']['max'],
step=1,
value=config['output_size']['value'],
description='Output nodes:'),
architecture=fixed(config['architecture']))
# Selector principal del modelo
interact(interactive_nn,
model_type=Dropdown(options={
'Simple Linear Regression': 'simple_linear_regression',
'Multiple Linear Regression': 'multiple_linear_regression',
'Multinomial Logistic Regression': 'multinomial_logistic_regression',
'MLP': 'mlp'
}, description='Model Type:'));
interactive(children=(Dropdown(description='Model Type:', options={'Simple Linear Regression': 'simple_linear_…
En una neurona (o unidad), el output es típicamente:
$$ y = f(\mathbf{w}^\top \mathbf{x} + b) $$Donde:
| Función | Explicación |
|---|---|
| Desplazar la activación | Permite que la neurona se active aunque todas las entradas sean cero |
| Ajustar la posición del umbral | Cambia el punto donde la activación cambia (como mover una recta) |
| Flexibiliza el modelo | Sin bias, la red está limitada a funciones que pasan por el origen |
| Facilita el aprendizaje | Mejora la capacidad de la red para ajustarse a patrones no centrados |
Imagina una neurona con función ReLU:
$$ f(x) = \max(0, w \cdot x + b) $$Esto permite mayor control sobre la región activa de la neurona.
Sí, al igual que los pesos:
nn.Linear, bias=True por defectoEl bias es crucial para que una red neuronal pueda aprender desplazamientos, ajustar límites de decisión y representar correctamente funciones no centradas. Sin él, la red se vuelve rígida y menos expresiva.
Una red de Hopfield es una red neuronal recurrente que almacena patrones como estados estables de la dinámica del sistema. Funciona como una memoria asociativa, capaz de recuperar un patrón completo a partir de una versión parcial o ruidosa.
La actualización de cada neurona se realiza mediante una suma ponderada de sus entradas:
$$ h_i = \sum_{j} w_{ij} s_j $$$$ s_i(t+1) = \begin{cases} 1 & \text{si } h_i \geq \theta_i \\ 0 & \text{si } h_i < \theta_i \end{cases} $$En una red de Hopfield binaria, el uso de una función de activación tipo umbral permite controlar la dinámica de la red. Al ajustar correctamente el umbral (bias), se logra que la red mantenga la misma actividad que los patrones aprendidos, lo que estabiliza la recuperación y evita estados espurios.
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import bernoulli
import ipywidgets as widgets
from ipywidgets import interact
def threshold_function(x, bias):
return np.where(x - bias >= 0, 1, 0)
def std_vec(x):
return (x - x.mean()) / x.std()
# Constants
N = 1000
k = 50
P = 10
time = 10
C = np.random.randint(0, N, size=(N, k))
def run_simulation(activity=0.25):
X = bernoulli.rvs(p=activity, size=(P, N))
X_std = (X - X.mean(axis=1, keepdims=True)) / X.std(axis=1, keepdims=True)
W = np.zeros(C.shape)
for xi in X_std:
for ni in range(N):
W[ni] += xi[C[ni]] * xi[ni]
a = activity
A = a * (1 - a)
bias = (1 - 2 * a) / (2 * np.sqrt(A))
M = []
for xi in X_std[:1]:
states = xi.copy()
m = [(std_vec(states) * std_vec(xi)).mean()]
neural_input = []
for ti in range(time):
neural_input = np.array([((std_vec(states[C[ni]]) * W[ni]).mean()) for ni in range(N)])
states = np.array([((std_vec(states[C[ni]]) * W[ni]).mean() - bias >= 0) for ni in range(N)])
m += [(std_vec(states) * std_vec(xi)).mean()]
if m[-2] == m[-1]:
break
M += [m[-1]]
x = np.linspace(-2, 4, 1000)
y = threshold_function(x, bias)
plt.figure(figsize=(15, 5))
plt.subplot(1,2,1)
plt.plot(x, y, label="f(x) = 1 if x >= threshold else 0")
plt.plot(neural_input, states, 'o', label="Neuron inputs and states", alpha=0.6)
plt.title(f"Threshold Function (activity={activity:.2f})")
plt.xlabel("x: neural input")
plt.ylabel("f(x): activation function")
plt.axhline(0, color="black", linewidth=0.5, linestyle="--")
plt.axvline(0, color="black", linewidth=0.5, linestyle="--")
plt.xlim(-4, 4)
plt.ylim(-0.1, 1.1)
plt.grid(alpha=0.3)
plt.legend()
plt.tight_layout()
plt.subplot(1,2,2)
plt.imshow(bernoulli.rvs(activity, size=(100,100)), cmap='gray')
plt.axis('off')
plt.show()
print(f"Bias: {bias:.2f}, Mean output state: {states.mean():.2f}, Mean input pattern: {X[0].mean():.2f}")
interact(run_simulation, activity=widgets.FloatSlider(value=0.5, min=0.1, max=0.9, step=0.1, description='Activity'));
interactive(children=(FloatSlider(value=0.5, description='Activity', max=0.9, min=0.1), Output()), _dom_classe…
Un MLP (Multilayer Perceptron) está compuesto por capas de la forma:
$$ \mathbf{z} = \mathbf{W} \cdot \mathbf{x} + \mathbf{b}, \quad \mathbf{a} = f(\mathbf{z}) $$Donde:
| Aspecto | Efecto del Bias |
|---|---|
| Desplazamiento del activador | Permite que la neurona se active aunque todas las entradas sean cero |
| Ajuste fino | Mejora la capacidad del modelo para aprender funciones con desplazamiento |
| Aprendizaje de umbrales | En activaciones como ReLU, el bias ajusta el punto donde la unidad se activa |
| Control de actividad | Puede usarse para mantener ciertas activaciones promedio (como en Hopfield) |
| Representación más rica | Aumenta la expresividad del modelo sin necesidad de más neuronas o capas |
| Red de Hopfield | MLP |
|---|---|
| Activación binaria, dinámica autónoma | Activación continua, entrenado con datos |
| Bias = threshold, fija actividad global | Bias = parámetro entrenable, mejora precisión |
| Controla la convergencia a patrones | Ajusta la frontera de decisión |
En Hopfield, el threshold fija una propiedad global de la dinámica (como la actividad media); en un MLP, el bias se aprende para ajustar localmente la respuesta de cada unidad y lograr un mejor rendimiento predictivo.
En un MLP, el bias permite desplazar las funciones de activación para mejorar el ajuste y la capacidad de representación del modelo. Aunque su función es diferente de la red de Hopfield, ambos casos comparten la idea de que el bias es esencial para un comportamiento flexible y ajustado a los datos o patrones esperados.
Gradient Descent (descenso del gradiente) es un algoritmo de optimización que busca minimizar una función ajustando sus parámetros paso a paso en la dirección del descenso más pronunciado (el gradiente negativo).
Dada una función $f(\theta)$ que queremos minimizar:
$$ \theta \leftarrow \theta - \eta \cdot \nabla f(\theta) $$Se repite este paso iterativamente hasta encontrar un mínimo local (idealmente, el global).
Imagina que estás bajando una colina con niebla. No puedes ver el fondo, pero puedes mirar tus pies y saber hacia dónde desciende el terreno. El gradiente te dice en qué dirección ir. El learning rate determina qué tan lejos avanzas en cada paso.
| Tipo | Descripción | Uso común en redes neuronales |
|---|---|---|
| Batch GD | Usa todo el dataset para cada paso | Poco usado (muy lento) |
| Stochastic GD | Usa 1 ejemplo por paso | Ruidoso pero rápido |
| Mini-batch GD | Usa pequeños lotes (batch) por paso | ✅ Estándar en deep learning |
En redes neuronales, gradient descent se usa para ajustar los pesos y bias de cada capa para minimizar una función de pérdida, como el error cuadrático medio o la entropía cruzada.
Donde:
| Aspecto | Rol del gradient descent |
|---|---|
| Aprendizaje | Permite a la red ajustar pesos con el error |
| Generalización | Una buena tasa de aprendizaje evita sobreajuste |
| Convergencia | Dirige el modelo hacia soluciones óptimas |
Por eso, a veces se usa:
import numpy as np
import matplotlib.pyplot as plt
import ipywidgets as widgets
from IPython.display import display, clear_output
from sympy import *
# Define the symbolic function and its derivative
x_sym = symbols('x')
fx = sin(x_sym - 10) + 0.05 * (x_sym - 10)**2 + 10
dfx_sym = diff(fx, x_sym)
# Convert to numpy functions
f = lambdify(x_sym, fx)
dfx = lambdify(x_sym, dfx_sym)
# Define tangent line
def line(x, x1, y1):
return dfx(x1) * (x - x1) + y1
# Main optimization function
def plot_gradient_descent(x0, lr, min_iterations, max_iterations, use_adaptive_lr):
clear_output(wait=True)
# Validate iteration range
if max_iterations < min_iterations:
max_iterations = min_iterations
a, b = 0, 20
epsilon = 0.00001
x = np.linspace(a, b, int((b - a) / epsilon))
y = f(x)
plt.figure(figsize=(10, 6))
plt.plot(x, y, label='f(x) = sin(x-10) + 0.05*(x-10)² + 10')
x0_l = [x0]
fx0_l = [f(x0)]
current_lr = lr
plt.scatter(x0, f(x0), color='r', s=100, label='Initial point')
xrange = np.linspace(max(a, x0-1), min(b, x0+1), 10)
plt.plot(xrange, line(xrange, x0, f(x0)), 'C2--', linewidth=2)
# Run gradient descent
for it in range(max_iterations):
gradient = dfx(x0)
if use_adaptive_lr:
current_lr = lr if it < 10 else 0.3
x0 = x0 - current_lr * gradient
x0_l.append(x0)
fx0_l.append(f(x0))
# Plot only if in the specified range
if it >= min_iterations - 1:
plt.scatter(x0, f(x0), color='C2', s=50, alpha=0.6)
xrange = np.linspace(max(a, x0-1), min(b, x0+1), 10)
plt.plot(xrange, line(xrange, x0, f(x0)), 'C2--', linewidth=2, alpha=0.6)
# Plot path and final point
plot_start_idx = max(0, min_iterations - 1)
plt.plot(x0_l[plot_start_idx:], fx0_l[plot_start_idx:], '-o', label='Optimization path')
plt.scatter(x0_l[-1], fx0_l[-1], color='k', s=200, label='Final point')
plt.xlabel('x')
plt.ylabel('f(x)')
plt.title('Gradient Descent Optimization')
plt.legend()
plt.grid(True)
plt.show()
# Create interactive widgets
x0_slider = widgets.FloatSlider(value=1, min=0, max=20, step=0.1, description='x0:')
lr_slider = widgets.FloatSlider(value=2.2, min=0, max=3, step=0.01, description='Learning Rate:')
min_iterations_slider = widgets.IntSlider(value=1, min=1, max=100, step=1, description='Min Iterations:')
max_iterations_slider = widgets.IntSlider(value=30, min=1, max=100, step=1, description='Max Iterations:')
adaptive_lr_toggle = widgets.Checkbox(value=False, description='Adaptive LR')
random_x0_button = widgets.Button(description='Random x0', button_style='info')
# Button callback
def on_random_x0_clicked(b):
x0_slider.value = np.random.random() * 20 # Random x0 in [0, 20]
out.outputs = () # Force update by clearing output
plot_gradient_descent(
x0_slider.value,
lr_slider.value,
min_iterations_slider.value,
max_iterations_slider.value,
adaptive_lr_toggle.value
)
random_x0_button.on_click(on_random_x0_clicked)
# Combine widgets into a UI
ui = widgets.VBox([
widgets.HTML(value='<h3>Gradient Descent Optimization</h3>'),
x0_slider,
lr_slider,
min_iterations_slider,
max_iterations_slider,
adaptive_lr_toggle,
random_x0_button
])
# Create interactive output
out = widgets.interactive_output(plot_gradient_descent, {
'x0': x0_slider,
'lr': lr_slider,
'min_iterations': min_iterations_slider,
'max_iterations': max_iterations_slider,
'use_adaptive_lr': adaptive_lr_toggle
})
# Display widgets and output
display(ui, out)
VBox(children=(HTML(value='<h3>Gradient Descent Optimization</h3>'), FloatSlider(value=1.0, description='x0:',…
Output()
Queremos encontrar la mejor línea recta $y = wx + b$ que ajuste un conjunto de puntos $(x_i, y_i)$. Esto es un problema de regresión lineal, y se puede resolver con descenso por gradiente.
La más común es el error cuadrático medio (MSE):
$$ L(w, b) = \frac{1}{n} \sum_{i=1}^n (y_i - (wx_i + b))^2 $$Esta función mide cuán lejos están los puntos predichos de los reales.
Calculamos el gradiente de la pérdida respecto a $w$ y $b$:
$$ \frac{\partial L}{\partial w} = -\frac{2}{n} \sum (y_i - (wx_i + b)) \cdot x_i $$$$ \frac{\partial L}{\partial b} = -\frac{2}{n} \sum (y_i - (wx_i + b)) $$Luego actualizamos:
$$ w \leftarrow w - \eta \cdot \frac{\partial L}{\partial w} \quad b \leftarrow b - \eta \cdot \frac{\partial L}{\partial b} $$donde $\eta$ es el learning rate.
Se ejecutan varias épocas hasta que el error deje de disminuir significativamente.
Si tus datos tienen rangos muy diferentes (por ejemplo, $x \in [1, 1000]$), el gradiente puede volverse muy inestable o muy lento en converger.
MinMaxScaler o StandardScaler para llevar $x$ e $y$ a rangos comparables.Esto mejora:
Porque el modelo fue entrenado en la escala transformada, pero los usuarios o sistemas necesitan resultados en la escala real del problema (por ejemplo, en dólares, grados, etc.).
Por eso, al final usamos
.inverse_transform()delscalerpara convertir las predicciones y compararlas con los datos reales.
El descenso del gradiente permite encontrar los parámetros $w, b$ que mejor ajustan una línea recta a los datos. Escalar los datos estabiliza y acelera ese proceso. Volver a la escala original garantiza que los resultados sean comprensibles y útiles.
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# Load data
df = pd.read_csv("https://raw.githubusercontent.com/marsgr6/r-scripts/master/data/data_gd_demo.csv", header=None)
df.columns = ['xi', 'yi'] # Assign column names
# Store original min/max before normalization
xi_min, xi_max = df['xi'].min(), df['xi'].max()
yi_min, yi_max = df['yi'].min(), df['yi'].max()
# Normalize data for gradient descent
df_normalized = (df - df.min()) / (df.max() - df.min())
# Gradient descent function (assumed)
def gradient_descent(m, b, data, L):
N = len(data)
m_grad = 0
b_grad = 0
for i in range(N):
x = data['xi'].iloc[i]
y = data['yi'].iloc[i]
y_pred = m * x + b
m_grad += -(2/N) * x * (y - y_pred)
b_grad += -(2/N) * (y - y_pred)
m = m - L * m_grad
b = b - L * b_grad
return m, b
# Denormalize parameters
def denormalize_parameters(m, b, xi_min, xi_max, yi_min, yi_max):
m_original = m * (yi_max - yi_min) / (xi_max - xi_min)
b_original = b * (yi_max - yi_min) + yi_min - m_original * xi_min
return m_original, b_original
# Plotting
learning_rates = [0.1, 0.5, 0.7]
steps = [10, 20, 50, 100, 500]
fig = plt.figure(figsize=(20, 20), dpi=80)
for cont1, learn in enumerate(learning_rates):
for cont2, step in enumerate(steps):
m = 0
b = 0
L = learn
epochs = step
for i in range(epochs):
m, b = gradient_descent(m, b, df_normalized, L)
# Denormalize parameters
m_original, b_original = denormalize_parameters(m, b, xi_min, xi_max, yi_min, yi_max)
# Generate predictions in original scale
linspace_normalized = np.linspace(df_normalized['xi'].min(), df_normalized['xi'].max(), 100)
linspace_original = linspace_normalized * (xi_max - xi_min) + xi_min
datax = [m_original * x + b_original for x in linspace_original]
ax1 = plt.subplot2grid((5, 5), (cont1, cont2))
ax1.scatter(df['xi'], df['yi'], label='Data', color="black")
ax1.title.set_text(f'LR: {learn} S:{step}')
ax1.plot(linspace_original, datax, color="red", label='Gradient descent')
ax1.set_xlabel('x')
ax1.set_ylabel('y')
ax1.legend()
plt.tight_layout()
plt.show()
El problema XOR (eXclusive OR) es uno de los ejemplos clásicos en la historia del aprendizaje automático y las redes neuronales, ya que pone en evidencia las limitaciones de los modelos lineales simples y motiva la necesidad de arquitecturas más complejas.
La operación XOR es una función lógica que toma dos entradas binarias (0 o 1) y devuelve 1 solo si las entradas son diferentes. Es decir:
| Entrada X1 | Entrada X2 | Salida XOR, y |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
Esta función no puede ser separada por una línea recta en el plano, lo que se conoce como un problema no linealmente separable.
Un perceptrón simple (una sola neurona) puede aprender funciones linealmente separables, como AND o OR. Sin embargo, XOR no lo es, ya que no existe una línea en el espacio 2D de entrada que pueda dividir correctamente las clases 0 y 1 de la salida.
Aunque usemos una función de activación no lineal (como la sigmoide o la ReLU), un único perceptrón no tiene la capacidad de crear una combinación adecuada de pesos y sesgos que separe correctamente las salidas deseadas del problema XOR. Esto fue demostrado formalmente por Minsky y Papert en los años 60, lo que llevó a un declive temporal en el interés por las redes neuronales.
La clave para resolver el problema XOR está en utilizar una Red Neuronal Multicapa (MLP, por sus siglas en inglés), que incluye:
La capa oculta introduce una transformación no lineal del espacio de entrada, lo que permite que el modelo aprenda representaciones internas más complejas. Con esta arquitectura, el MLP puede “curvar” el espacio de entrada de manera que la función XOR se vuelva separable en ese nuevo espacio transformado.
Un ejemplo simple de arquitectura que puede resolver XOR es:
tanh o sigmoid),La red aprende a combinar las salidas de la capa oculta de tal forma que pueda aproximar correctamente la salida XOR deseada.
Gráficamente, si se representan los puntos (X1, X2) en un plano:
No hay una línea recta que separe estas clases correctamente. Pero en un espacio transformado por la capa oculta, esta separación sí se puede lograr.

El problema XOR es crucial porque:
La figura presentada muestra la evolución del error cuadrático medio (MSE) durante el proceso de entrenamiento de un perceptrón multicapa (MLP) aplicado al problema clásico de la función lógica XOR. Se comparan dos configuraciones: una que incorpora neuronas de sesgo (Bias True en azul) y otra que no las utiliza (Bias False en naranja).

En la curva correspondiente al modelo con bias (Bias True), se observa un comportamiento típico de convergencia efectiva en redes neuronales: tras una etapa inicial de oscilaciones debido al proceso estocástico del entrenamiento (aproximadamente hasta la iteración 2000), el error comienza a decrecer de manera sostenida, alcanzando valores cercanos a cero después de la iteración 6000. Este comportamiento indica que el modelo logra aprender con precisión la función XOR, lo cual se corrobora con las salidas finales del modelo:
Predicciones: [[0.036, 0.960, 0.960, 0.050]]
Salidas esperadas: [[0, 1, 1, 0]]
Estas predicciones reflejan una aproximación muy precisa a los valores esperados, confirmando la capacidad del MLP con bias para resolver relaciones no linealmente separables.
En contraste, el modelo sin bias (Bias False) presenta una curva de error que permanece estancada en torno a un valor de ~0.25 durante una gran parte del entrenamiento. A partir de la iteración 6000 se aprecia una ligera disminución, aunque el modelo converge a un valor de MSE significativamente más alto (~0.12), sin alcanzar una representación adecuada de la función objetivo. Las predicciones obtenidas al final del entrenamiento son:
Predicciones: [[0.126, 0.777, 0.692, 0.587]]
Salidas esperadas: [[0, 1, 1, 0]]
Estas predicciones muestran una clara desviación respecto a los valores esperados, especialmente en los patrones (0,0) y (1,1), lo que evidencia una limitación estructural del modelo sin bias para ajustar funciones no lineales.
Los resultados empíricos respaldan un principio fundamental del diseño de redes neuronales: la inclusión de neuronas de sesgo es crucial para aumentar la capacidad expresiva del modelo. El sesgo permite a cada neurona ajustar su umbral de activación independientemente de la entrada, facilitando así la representación de funciones no linealmente separables como XOR. La ausencia de bias restringe esta capacidad, incluso cuando se emplean funciones de activación no lineales.
En síntesis, la evidencia numérica y gráfica indica que el uso de bias no solo mejora la convergencia del entrenamiento, sino que resulta indispensable para que el modelo logre una solución satisfactoria al problema XOR.
import numpy as np
import matplotlib.pyplot as plt
from sklearn.metrics import mean_squared_error
import warnings
warnings.filterwarnings('ignore')
# XOR input and output (convert to arrays to avoid matrix issues)
XOR_inputs = np.array([[0, 1, 0, 1], [0, 0, 1, 1]]) # shape: (2, 4)
XOR_outputs = np.array([[0, 1, 1, 0]]) # shape: (1, 4)
def mlp(X, Y, n_input_base, n_hidden, n_output,
training_steps=5000, lr=0.7, batch_size=1, use_bias=True):
# Add bias to input layer if requested
if use_bias:
inputs = np.vstack((X, np.ones((1, X.shape[1])))) # shape: (n_input_base + 1, 4)
n_input = n_input_base + 1
else:
inputs = X # shape: (n_input_base, 4)
n_input = n_input_base
expected_outputs = Y # shape: (1, 4)
# Initialize weights
weights_hidden = np.random.rand(n_hidden, n_input) - 0.5 # shape: (n_hidden, n_input)
if use_bias:
weights_output = np.random.rand(n_output, n_hidden + 1) - 0.5 # bias for hidden layer
else:
weights_output = np.random.rand(n_output, n_hidden) - 0.5 # no bias
mse_history = []
for step in range(training_steps):
for _ in range(batch_size):
# Randomly select a sample
sample_idx = np.random.randint(0, inputs.shape[1])
input_sample = inputs[:, sample_idx].reshape(-1, 1) # shape: (n_input, 1)
# Forward pass: input to hidden
hidden_activations = 1 / (1 + np.exp(-weights_hidden @ input_sample)) # shape: (n_hidden, 1)
# Add bias to hidden layer if applicable
if use_bias:
hidden_activations = np.vstack((hidden_activations, np.ones((1, 1)))) # shape: (n_hidden + 1, 1)
# Forward pass: hidden to output
output_activations = 1 / (1 + np.exp(-weights_output @ hidden_activations)) # shape: (n_output, 1)
# Compute output deltas
output_deltas = (expected_outputs[:, sample_idx].reshape(-1, 1) - output_activations) * \
output_activations * (1 - output_activations) # shape: (n_output, 1)
# Backpropagation: compute gradient to hidden layer
gradient_to_hidden = weights_output.T @ output_deltas # shape: (n_hidden + 1, 1) or (n_hidden, 1)
# Compute hidden deltas (exclude bias from gradient if applicable)
if use_bias:
hidden_activations_no_bias = hidden_activations[:-1, :] # shape: (n_hidden, 1)
gradient_to_hidden = gradient_to_hidden[:-1, :] # shape: (n_hidden, 1)
else:
hidden_activations_no_bias = hidden_activations # shape: (n_hidden, 1)
# Convert to arrays to ensure element-wise multiplication
hidden_activations_no_bias = np.array(hidden_activations_no_bias)
gradient_to_hidden = np.array(gradient_to_hidden)
# Element-wise multiplication for hidden deltas
hidden_deltas = hidden_activations_no_bias * (1 - hidden_activations_no_bias) * gradient_to_hidden # shape: (n_hidden, 1)
# Update weights
weights_output += lr * (output_deltas @ hidden_activations.T) # shape: (n_output, n_hidden + 1)
weights_hidden += lr * (hidden_deltas @ input_sample.T) # shape: (n_hidden, n_input)
# Evaluate MSE on all patterns
hidden_all = 1 / (1 + np.exp(-weights_hidden @ inputs)) # shape: (n_hidden, 4)
if use_bias:
hidden_all = np.vstack((hidden_all, np.ones((1, hidden_all.shape[1])))) # shape: (n_hidden + 1, 4)
output_all = 1 / (1 + np.exp(-weights_output @ hidden_all)) # shape: (n_output, 4)
mse_history.append(mean_squared_error(expected_outputs, output_all))
return mse_history, output_all, weights_hidden, weights_output
# Run MLP with or without bias
use_bias = True
n_input_base = 2
mse_history_bt, final_output, weights_hidden_bt, weights_output_bt = mlp(
XOR_inputs, XOR_outputs,
n_input_base=n_input_base,
n_hidden=2,
n_output=1,
training_steps=10000,
lr=0.7,
batch_size=1,
use_bias=use_bias
)
print("Bias True")
# Print final prediction vs expected
print("Final predictions:")
print(np.round(final_output, 3))
print("Expected outputs:")
print(XOR_outputs)
# Run MLP with or without bias
use_bias = False
n_input_base = 2
mse_history_bf, final_output, weights_hidden_bf, weights_output_bf = mlp(
XOR_inputs, XOR_outputs,
n_input_base=n_input_base,
n_hidden=2,
n_output=1,
training_steps=10000,
lr=0.7,
batch_size=1,
use_bias=use_bias
)
print("Bias False")
# Print final prediction vs expected
print("Final predictions:")
print(np.round(final_output, 3))
print("Expected outputs:")
print(XOR_outputs)
# Plot training error
plt.plot(mse_history_bt, label="Bias True")
# Plot training error
plt.plot(mse_history_bf, label="Bias False")
plt.title("Training MSE (bias vs no bias)")
plt.xlabel("Iteration")
plt.ylabel("Mean Squared Error")
plt.grid(True)
plt.legend()
plt.show()
Bias True Final predictions: [[0.035 0.957 0.957 0.051]] Expected outputs: [[0 1 1 0]] Bias False Final predictions: [[0.104 0.768 0.76 0.575]] Expected outputs: [[0 1 1 0]]
La figura muestra las superficies de decisión aprendidas por un perceptrón multicapa (MLP) entrenado para aproximar la función lógica XOR. Se presentan dos configuraciones: a la izquierda, el modelo sin bias; a la derecha, el modelo con bias. Las regiones coloreadas representan el valor de salida del MLP en función de las entradas $X_1$ y $X_2$, interpoladas sobre una malla continua. Los puntos marcados corresponden a las cuatro combinaciones binarias de entrada (0,0), (0,1), (1,0), (1,1), con sus clases objetivo representadas como:

En la configuración sin neuronas de sesgo, la superficie de decisión muestra una separación inadecuada de las regiones de clase. Específicamente:
En contraste, la superficie aprendida por el modelo con bias muestra:
La visualización respalda la evidencia empírica obtenida a partir del MSE: el modelo con bias logra representar adecuadamente la no linealidad inherente al problema XOR, mientras que el modelo sin bias falla en capturar esta complejidad estructural.
Desde el punto de vista del aprendizaje automático, esto demuestra que el uso de neuronas de sesgo permite que cada unidad neuronal aprenda una traslación del plano de decisión, incrementando así la capacidad de representación del modelo. Sin esta flexibilidad, el modelo queda restringido a combinaciones lineales del espacio de entrada, lo cual resulta insuficiente para tareas como XOR.
# Contour plot function with fixed colorbar scale
def contour_plot(w_h, w_o, bias, bar=False, title="Decision Surface", r_i=None, r_d=None):
"""
Plot the decision surface of an MLP classifier with a fixed colorbar scale [0, 1].
Parameters:
w_h (np.ndarray): Hidden layer weights, shape (n_hidden, n_input)
w_o (np.ndarray): Output layer weights, shape (n_output, n_hidden) or (n_output, n_hidden + 1)
bias (str): "True" or "False" to indicate if bias was used
title (str): Custom title for the plot
r_i (np.ndarray): Input data, shape (n_input_base, n_samples), e.g., (2, 4)
r_d (np.ndarray): Target data, shape (n_samples,), e.g., (4,)
"""
try:
w_h = np.array(w_h)
w_o = np.array(w_o)
n_classes = 2
plot_colors = "br"
plot_markers = "oP"
x_min, x_max = -0.1, 1.1
y_min, y_max = -0.1, 1.1
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
np.linspace(y_min, y_max, 100))
grid_points = np.c_[xx.ravel(), yy.ravel()].T
if bias.lower() == "true":
if w_h.shape[1] != 3:
raise ValueError(f"Expected w_h with bias to have shape (n_hidden, 3), got {w_h.shape}")
grid_points = np.vstack((grid_points, np.ones((1, grid_points.shape[1]))))
else:
if w_h.shape[1] != 2:
raise ValueError(f"Expected w_h without bias to have shape (n_hidden, 2), got {w_h.shape}")
hidden_activations = 1 / (1 + np.exp(-w_h @ grid_points))
if bias.lower() == "true":
if w_o.shape[1] != w_h.shape[0] + 1:
raise ValueError(f"Expected w_o with bias to have shape (n_output, {w_h.shape[0] + 1}), got {w_o.shape}")
hidden_activations = np.vstack((hidden_activations, np.ones((1, hidden_activations.shape[1]))))
else:
if w_o.shape[1] != w_h.shape[0]:
raise ValueError(f"Expected w_o without bias to have shape (n_output, {w_h.shape[0]}), got {w_o.shape}")
Z = 1 / (1 + np.exp(-w_o @ hidden_activations))
Z = Z.reshape(xx.shape)
cs = plt.contourf(xx, yy, Z, cmap=plt.cm.seismic, alpha=0.8, vmin=0, vmax=1)
csl = plt.contour(xx, yy, Z, colors='black', linewidths=0.5, vmin=0, vmax=1)
if bar:
cbar = plt.colorbar(cs)
cbar.ax.set_ylabel('MLP Output')
cbar.set_ticks([0, 0.5, 1])
cbar.add_lines(csl)
plt.xlabel('X1')
plt.ylabel('X2')
if r_i is None or r_d is None:
r_i = np.array([[0, 1, 0, 1], [0, 0, 1, 1]])
r_d = np.array([0, 1, 1, 0])
if r_i.shape[1] != r_d.shape[0]:
raise ValueError(f"r_i samples ({r_i.shape[1]}) must match r_d samples ({r_d.shape[0]})")
t_names = ["Class 0", "Class 1"]
for i, color, m in zip(range(n_classes), plot_colors, plot_markers):
idx = np.where(r_d == i)[0]
plt.scatter(r_i[0, idx], r_i[1, idx], c=color, label=t_names[i],
edgecolor='black', s=100, marker=m)
plt.title(f"{title} (Bias: {bias})")
return {'xx': xx, 'yy': yy, 'Z': Z}
except Exception as e:
print(f"Error in contour_plot: {e}")
raise
# Example usage with MLP weights trained with use_bias=False
# Assuming weights_hidden_bt and weights_output_bt are from MLP with use_bias=False
plt.figure(figsize=(12,4))
plt.subplot(1,2,1)
results_bf = contour_plot(weights_hidden_bf, weights_output_bf, "False")
plt.subplot(1,2,2)
results_bt = contour_plot(weights_hidden_bt, weights_output_bt, "True", bar=True)
La figura muestra las superficies 3D de salida del MLP en el espacio de entrada $(X_1, X_2)$, donde se observa el efecto de incluir o no unidades de sesgo (bias) en la red.
Sin bias (izquierda): la superficie es monótona y suavemente inclinada, indicando una representación funcional limitada. El modelo no logra generar las no linealidades necesarias para separar correctamente las clases del problema XOR.
Con bias (derecha): se observa una superficie con curvaturas complejas, que permite identificar regiones diferenciadas de activación. Esto refleja una capacidad de representación significativamente mayor, lo cual es esencial para aproximar funciones como XOR que no son linealmente separables.
Estas superficies confirman que la inclusión de bias en la arquitectura del MLP aumenta la dimensionalidad efectiva del espacio de hipótesis, habilitando soluciones no lineales que capturan con precisión la estructura del problema.

# Create figure with two 3D subplots
fig = plt.figure(figsize=(10, 4))
# Subplot 1: Bias=False
ax1 = fig.add_subplot(121, projection='3d')
surf1 = ax1.plot_surface(results_bf['xx'], results_bf['yy'], results_bf['Z'], cmap=plt.cm.RdYlBu_r,
linewidth=0, antialiased=False)
ax1.view_init(30, -60)
ax1.set_xlabel('X')
ax1.set_ylabel('Y')
ax1.set_zlabel('Network response')
plt.tight_layout()
# Subplot 2: Bias=False
ax2 = fig.add_subplot(122, projection='3d')
surf2 = ax2.plot_surface(results_bt['xx'], results_bt['yy'], results_bt['Z'], cmap=plt.cm.RdYlBu_r,
linewidth=0, antialiased=False)
ax2.view_init(30, -60)
ax2.set_xlabel('X')
ax2.set_ylabel('Y')
ax2.set_zlabel('Network response')
plt.tight_layout()
Un contour plot (o mapa de contornos) representa superficies tridimensionales en 2D mediante líneas que conectan puntos con igual valor, comúnmente utilizadas para mostrar elevación (en topografía) o intensidad (en mapas científicos).
🔹 Líneas cerradas indican colinas (si los valores aumentan hacia el centro) o depresiones (si disminuyen).
🔹 Líneas muy juntas indican pendientes empinadas; líneas más separadas representan pendientes suaves.
🔹 La forma de las líneas sugiere la silueta del terreno: picos, valles, laderas o mesetas.
🔹 El color (si está presente) puede ayudarte a distinguir elevaciones: tonos más claros o cálidos (como amarillo o rojo) suelen representar alturas mayores, mientras que tonos fríos (como azul) indican zonas más bajas o depresiones.
En resumen, los contornos muestran la forma del relieve, y el color, cuando se usa, añade información visual sobre la altitud o profundidad relativa.

Un Perceptrón Multicapa (MLP) es una red neuronal compuesta por varias capas de neuronas:
Cada neurona en una capa está conectada a todas las neuronas de la siguiente, y los pesos sinápticos determinan la fuerza de cada conexión. Las redes MLP son entrenadas supervisadamente para que, a partir de datos de entrada, aprendan a producir salidas correctas ajustando sus pesos internos.
En la capa oculta, cada neurona calcula una combinación lineal de las entradas (incluyendo un sesgo, si se activa) y aplica una función de activación no lineal, típicamente la sigmoide:
$$ a = \sigma(w \cdot x + b) $$
Este proceso produce una salida $\hat{y}$, la predicción del modelo.
Una vez obtenida la predicción, se calcula el error entre la salida esperada $y$ y la predicha $\hat{y}$. En este caso se utiliza el error cuadrático medio (MSE):
$$ \text{MSE} = \frac{1}{N} \sum_{i=1}^{N} (y_i - \hat{y}_i)^2 $$La red usa el error calculado para ajustar los pesos hacia atrás, desde la capa de salida hacia la capa oculta. Para ello, se aplican derivadas de las funciones de activación (sigmoide en este caso), lo que permite calcular cuánto contribuyó cada peso al error total.
Se calcula el delta de la salida, que combina el error y la derivada de la sigmoide:
$$ \delta_o = (y - \hat{y}) \cdot \hat{y} \cdot (1 - \hat{y}) $$
Luego se propaga este delta hacia atrás para obtener los deltas de la capa oculta, también usando la derivada de la sigmoide:
$$ \delta_h = a_h \cdot (1 - a_h) \cdot (w_o^T \cdot \delta_o) $$
(donde $a_h$ son las activaciones ocultas y $w_o$ los pesos de salida).
Una vez calculados los deltas, se actualizan los pesos de cada capa en la dirección que reduce el error, usando la regla general del descenso del gradiente:
$$ w := w + \eta \cdot \delta \cdot a^T $$donde:
Este ajuste se hace tanto para:
Sí, en su forma extendida. La regla delta clásica (usada en perceptrones simples) dice:
$$ \Delta w = \eta \cdot (y - \hat{y}) \cdot x $$En este MLP, se generaliza:
El MLP implementado:
La clasificación binaria es un tipo de problema de aprendizaje supervisado en el que el modelo debe predecir una de dos clases posibles, por ejemplo:
El modelo recibe entradas (features) y debe aprender a asignar una etiqueta 0 o 1 (o cualquier par de clases binarias).
scikit-learn?¶Un MLPClassifier (Perceptrón Multicapa) es una red neuronal con al menos una capa oculta, capaz de aprender patrones complejos y no lineales en los datos.
# Paso 1: importar el modelo
from sklearn.neural_network import MLPClassifier
# Paso 2: definir el modelo con hiperparámetros
mlp = MLPClassifier(hidden_layer_sizes=(100,), activation='relu',
solver='adam', learning_rate_init=0.001,
max_iter=1000, random_state=42)
# Paso 3: entrenar (ajustar) el modelo con datos de entrenamiento
mlp.fit(X_train, y_train)
# Paso 4: hacer predicciones y evaluar el modelo
y_pred = mlp.predict(X_test)
Antes de entrenar el modelo, se divide el conjunto de datos en dos partes:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42)
X_train, y_train).X_test, y_test).Esta separación permite medir si el modelo generaliza bien a datos no vistos.
Las redes neuronales son sensibles a las escalas de los datos, por lo que es buena práctica escalar los valores de entrada:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
Esto asegura que todas las variables de entrada estén en un rango comparable, lo que acelera y mejora el proceso de entrenamiento.
$$ \text{Precision} = \frac{\text{Verdaderos Positivos}}{\text{Verdaderos Positivos + Falsos Positivos}} $$¿De todas las veces que el modelo predijo una clase, cuántas veces acertó?
$$ \text{Recall} = \frac{\text{Verdaderos Positivos}}{\text{Verdaderos Positivos + Falsos Negativos}} $$¿De todos los casos que realmente eran de una clase, cuántos fueron detectados correctamente?
$$ \text{F1-score} = 2 \cdot \frac{\text{Precision} \cdot \text{Recall}}{\text{Precision + Recall}} $$Es el promedio armonizado de precision y recall. Balancea ambos errores (FP y FN).
$$ \text{Accuracy} = \frac{\text{TP + TN}}{\text{Total de ejemplos}} $$¿Qué porcentaje total de predicciones fueron correctas?
Número de instancias reales de cada clase en los datos de prueba.
75 ejemplos (balanceado).$$ \text{Macro avg} = \frac{\text{Métrica clase 0} + \text{Métrica clase 1}}{2} $$Promedio simple de precision, recall y F1 entre todas las clases, sin importar cuántos ejemplos haya por clase.
$$ \text{Weighted avg} = \frac{\sum (\text{support}_i \cdot \text{métrica}_i)}{\sum \text{support}_i} $$Promedio ponderado por la cantidad de ejemplos por clase (support).
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
from sklearn.metrics import classification_report, confusion_matrix
import ipywidgets as widgets
from IPython.display import display
# Generar el dataset
X, y = make_moons(n_samples=500, noise=0.25, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
# Función para trazar la frontera de decisión
def plot_decision_boundary(model, X, y):
h = 0.02
x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
np.arange(y_min, y_max, h))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
Z = Z.reshape(xx.shape)
plt.figure(figsize=(8, 6))
plt.contourf(xx, yy, Z, cmap=plt.cm.Paired, alpha=0.6)
plt.scatter(X[:, 0], X[:, 1], c=y, edgecolors='k', cmap=plt.cm.Paired)
plt.title("Frontera de decisión del MLP")
plt.xlabel("x1")
plt.ylabel("x2")
plt.show()
# Función que se actualiza con el widget
def train_and_plot(neurons):
mlp = MLPClassifier(hidden_layer_sizes=(neurons,), random_state=42)
mlp.fit(X_train_scaled, y_train)
y_pred = mlp.predict(X_test_scaled)
print(f"Neurons in hidden layer: {neurons}")
print("\nMatriz de confusión:\n", confusion_matrix(y_test, y_pred))
print("\nReporte de clasificación:\n", classification_report(y_test, y_pred))
plot_decision_boundary(mlp, X_test_scaled, y_test)
# Widget selector
neurons_selector = widgets.Dropdown(
options=[10, 100, 1000],
value=100,
description='Neurons:',
style={'description_width': 'initial'}
)
# Mostrar el widget
widgets.interact(train_and_plot, neurons=neurons_selector);
interactive(children=(Dropdown(description='Neurons:', index=1, options=(10, 100, 1000), style=DescriptionStyl…
| Predicho 0 | Predicho 1 | |
|---|---|---|
| Real 0 | 69 | 6 |
| Real 1 | 12 | 63 |
| Clase | Precision | Recall | F1-score | Apoyo (support) |
|---|---|---|---|---|
| 0 | 0.85 | 0.92 | 0.88 | 75 |
| 1 | 0.91 | 0.84 | 0.87 | 75 |
Accuracy (exactitud):
$$ \frac{69 + 63}{150} = 0.88 $$

import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve, auc, precision_recall_curve, average_precision_score
mlp = MLPClassifier(hidden_layer_sizes=(100,), random_state=42)
mlp.fit(X_train_scaled, y_train)
y_pred = mlp.predict(X_test_scaled)
# Obtener probabilidades de clase positiva
y_score = mlp.predict_proba(X_test_scaled)[:, 1]
# ROC curve
fpr, tpr, _ = roc_curve(y_test, y_score)
roc_auc = auc(fpr, tpr)
# Precision-Recall curve
precision, recall, _ = precision_recall_curve(y_test, y_score)
avg_precision = average_precision_score(y_test, y_score)
# Crear figura con 2 subplots
fig, axs = plt.subplots(1, 2, figsize=(12, 5))
# Panel 1: Curva ROC
axs[0].plot(fpr, tpr, color='blue', label=f'AUC = {roc_auc:.2f}')
axs[0].plot([0, 1], [0, 1], color='gray', linestyle='--')
axs[0].set_title('Curva ROC')
axs[0].set_xlabel('False Positive Rate')
axs[0].set_ylabel('True Positive Rate')
axs[0].legend(loc='lower right')
axs[0].grid(True)
# Panel 2: Curva Precision-Recall
axs[1].plot(recall, precision, color='green', label=f'AP = {avg_precision:.2f}')
axs[1].set_title('Curva Precision-Recall')
axs[1].set_xlabel('Recall')
axs[1].set_ylabel('Precision')
axs[1].legend(loc='upper right')
axs[1].grid(True)
plt.tight_layout()
Ejes:
Ejes:
make_moons hay balance.make_moons:¶
Objetivo: Evaluar cómo cambia la tasa de verdaderos positivos (TPR) y falsos positivos (FPR) al variar el umbral de decisión.
1. Obtener las probabilidades de clase positiva del modelo para cada muestra:
y_scores = modelo.predict_proba(X)[:, 1]
2. Definir una lista de posibles umbrales (por ejemplo, ordenando y_scores)
3. Para cada umbral en la lista:
a. Convertir las probabilidades a etiquetas (0 o 1) usando el umbral
b. Comparar con las etiquetas reales para contar:
- TP: verdaderos positivos
- FP: falsos positivos
- TN: verdaderos negativos
- FN: falsos negativos
c. Calcular:
- TPR = TP / (TP + FN)
- FPR = FP / (FP + TN)
d. Guardar el par (FPR, TPR)
4. Graficar todos los puntos (FPR, TPR) y conectar con líneas
5. Calcular AUC como el área bajo la curva (usando integración o sumatoria trapezoidal)
Objetivo: Evaluar cómo cambia la precisión y el recall al variar el umbral de decisión.
1. Obtener las probabilidades de clase positiva del modelo para cada muestra:
y_scores = modelo.predict_proba(X)[:, 1]
2. Definir una lista de posibles umbrales (por ejemplo, ordenando y_scores)
3. Para cada umbral en la lista:
a. Convertir las probabilidades a etiquetas (0 o 1) usando el umbral
b. Comparar con las etiquetas reales para contar:
- TP: verdaderos positivos
- FP: falsos positivos
- FN: falsos negativos
c. Calcular:
- Precision = TP / (TP + FP) → cuidado con división por cero
- Recall = TP / (TP + FN)
d. Guardar el par (Recall, Precision)
4. Graficar los puntos (Recall, Precision) conectados por líneas
5. Calcular Average Precision (AP) como el área bajo la curva PR
La diferencia entre nuestro modelo MLP implementado manualmente y el modelo MLPClassifier de scikit-learn no solo está en el código, sino también en la flexibilidad, optimizaciones internas, y funcionalidades adicionales. A continuación se detallan las diferencias clave:
sklearn.MLPClassifier¶| Aspecto | Tu implementación manual | sklearn.MLPClassifier |
|---|---|---|
| Arquitectura | Solo 1 capa oculta, definida explícitamente en el código. | Soporta múltiples capas ocultas (hidden_layer_sizes=(100,50,30)), flexibles. |
| Activación | Solo función sigmoide (1 / (1 + exp(-x))). |
Soporta relu, tanh, logistic (sigmoid) y identity. |
| Inicialización | Pesos aleatorios simples (rand - 0.5). |
Inicialización más robusta (basada en heurísticas como Xavier/He). |
| Sesgo (bias) | Opcional (use_bias=True/False). |
Siempre incluido automáticamente. |
| Algoritmo de optimización | Descenso del gradiente básico con tasa fija. | adam, sgd, o lbfgs con control avanzado de tasa de aprendizaje, momentum, etc. |
| Retropropagación | Manual, con cálculo explícito de deltas. | Interno, optimizado, con autograd. |
| Entrenamiento | Una sola muestra por vez (batch size = 1). | Soporta entrenamiento por lotes (batch_size), mini-batch y por épocas. |
| Detección de convergencia | No la incluye. Corre todas las training_steps. |
Tiene control de convergencia, tolerancia (tol), parada temprana (early_stopping). |
| Regularización | No implementada. | Soporta L2 (alpha), dropout (si usas otros frameworks), early stopping. |
| Escalado de datos | Debe hacerse manualmente. | Espera que el usuario lo haga, pero está optimizado para trabajar con datos escalados. |
| Funciones de evaluación | Tienes que calcular métricas manualmente. | Compatible con toda la API de métricas de sklearn. |
| Velocidad y eficiencia | Adecuada para enseñanza y problemas simples. | Mucho más rápido y estable para problemas reales. |
MLPClassifier de sklearn es una herramienta de producción y experimentación, optimizada, robusta y configurable para muchas tareas reales.Se utilizará un modelo MLPRegressor para predecir el precio de las viviendas (PRICE) en el dataset Boston Housing, utilizando como variables predictoras RM (número promedio de habitaciones por vivienda) y LSTAT (porcentaje de población con bajo nivel socioeconómico). Se comparará su desempeño con un modelo de regresión polinómica de segundo grado (degree = 2) ajustado con PolynomialFeatures y LinearRegression, y se mostrarán las superficies de respuesta generadas por ambos modelos para analizar sus diferencias en la capacidad de capturar relaciones no lineales.
polyreg = make_pipeline(PolynomialFeatures(degree), LinearRegression())
significa lo siguiente:
Está creando un modelo de regresión polinómica combinando dos pasos:
PolynomialFeatures(degree):
Esta función transforma las variables originales (RM y LSTAT) generando nuevas características que incluyen potencias y productos cruzados hasta el grado especificado.
Por ejemplo, si degree = 2 y tus variables son x1 = RM y x2 = LSTAT, se generarán:
1 (intercepto), x1, x2, x1^2, x1*x2, x2^2.LinearRegression():
Una vez que se han creado las características polinómicas, este modelo ajusta una regresión lineal sobre esas nuevas variables. Aunque el modelo es lineal en los coeficientes, puede capturar relaciones no lineales en los datos gracias a las transformaciones previas.
make_pipeline?¶El uso de make_pipeline(...) permite encadenar automáticamente esos pasos como si fueran un solo modelo. Así, cuando haces polyreg.fit(X, y), se aplican en orden:
PolynomialFeatures.LinearRegression sobre esas nuevas variables.Este modelo no ajusta una recta (como la regresión lineal estándar), sino una superficie curva (en este caso, un polinomio de segundo grado), lo cual permite modelar relaciones más complejas entre las variables RM, LSTAT y PRICE.
import numpy as np
import pandas as pd
from sklearn.preprocessing import PolynomialFeatures, StandardScaler
from sklearn.linear_model import LinearRegression
from sklearn.neural_network import MLPRegressor
from sklearn.pipeline import make_pipeline
from sklearn.metrics import mean_squared_error
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Load and preprocess Boston Housing dataset
data = pd.read_csv("https://raw.githubusercontent.com/marsgr6/r-scripts/refs/heads/master/data/viz_data/boston_housing.csv")
X = data[['RM', 'LSTAT']] # RM (index 5), LSTAT (index 12)
y = data['PRICE']
# Convert to NumPy arrays to avoid InvalidIndexError with [:, i]
X_np = X.to_numpy()
y_np = y.to_numpy()
# Scale features for MLPRegressor
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_np)
# Fit models
degree = 2
polyreg = make_pipeline(PolynomialFeatures(degree), LinearRegression())
polyreg.fit(X_np, y_np)
mlp = MLPRegressor(hidden_layer_sizes=(100,), max_iter=1000, random_state=42)
mlp.fit(X_scaled, y_np)
# Create grid for prediction
x_min, x_max = X_np[:, 0].min() - 0.5, X_np[:, 0].max() + 0.5
y_min, y_max = X_np[:, 1].min() - 0.5, X_np[:, 1].max() + 0.5
xx, yy = np.meshgrid(np.linspace(x_min, x_max, 100),
np.linspace(y_min, y_max, 100))
grid_points = np.c_[xx.ravel(), yy.ravel()]
Z_poly = polyreg.predict(grid_points).reshape(xx.shape)
grid_points_scaled = scaler.transform(grid_points)
Z_mlp = mlp.predict(grid_points_scaled).reshape(xx.shape)
# Visualization limits
vmin = float(min(y_np.min(), Z_poly.min(), Z_mlp.min()))
vmax = float(max(y_np.max(), Z_poly.max(), Z_mlp.max()))
# Create Plotly figure
fig = make_subplots(
rows=2, cols=2,
specs=[[{'type': 'surface'}, {'type': 'surface'}],
[{'type': 'scatter'}, {'type': 'scatter'}]],
subplot_titles=('Polynomial Regression Surface', 'MLPRegressor Surface',
'Polynomial Regression Scatter', 'MLPRegressor Scatter'),
horizontal_spacing=0.1,
vertical_spacing=0.1
)
# 1) 3D Surface: Polynomial Regression
fig.add_trace(
go.Surface(
x=xx, y=yy, z=Z_poly,
colorscale='RdYlBu_r', cmin=vmin, cmax=vmax,
opacity=0.8,
showscale=False # ocultar barra en esta superficie
),
row=1, col=1
)
fig.add_trace(
go.Scatter3d(
x=X_np[:, 0], y=X_np[:, 1], z=y_np,
mode='markers',
marker=dict(size=5, color=y_np, colorscale='RdYlBu_r',
cmin=vmin, cmax=vmax,
line=dict(width=1, color='black')),
showlegend=False
),
row=1, col=1
)
# 2) 3D Surface: MLPRegressor
fig.add_trace(
go.Surface(
x=xx, y=yy, z=Z_mlp,
colorscale='RdYlBu_r', cmin=vmin, cmax=vmax,
opacity=0.8,
showscale=False # ocultar barra en esta superficie
),
row=1, col=2
)
fig.add_trace(
go.Scatter3d(
x=X_np[:, 0], y=X_np[:, 1], z=y_np,
mode='markers',
marker=dict(size=5, color=y_np, colorscale='RdYlBu_r',
cmin=vmin, cmax=vmax,
line=dict(width=1, color='black')),
showlegend=False
),
row=1, col=2
)
# 3) 2D Heatmap + Scatter: Polynomial Regression (deja UNA sola colorbar aquí)
fig.add_trace(
go.Heatmap(
x=xx[0, :], y=yy[:, 0], z=Z_poly,
colorscale='RdYlBu_r', zmin=vmin, zmax=vmax,
opacity=0.8,
colorbar=dict(
title='Price ($1000s)',
len=0.8,
thickness=15
)
),
row=2, col=1
)
fig.add_trace(
go.Scatter(
x=X_np[:, 0], y=X_np[:, 1],
mode='markers',
marker=dict(size=8, color=y_np, colorscale='RdYlBu_r',
cmin=vmin, cmax=vmax,
line=dict(width=1, color='black')),
text=[f'Price: {price:.1f}' for price in y_np],
hoverinfo='x+y+text',
showlegend=False
),
row=2, col=1
)
# 4) 2D Heatmap + Scatter: MLPRegressor (sin barra adicional)
fig.add_trace(
go.Heatmap(
x=xx[0, :], y=yy[:, 0], z=Z_mlp,
colorscale='RdYlBu_r', zmin=vmin, zmax=vmax,
opacity=0.8,
showscale=False # sin segunda colorbar
),
row=2, col=2
)
fig.add_trace(
go.Scatter(
x=X_np[:, 0], y=X_np[:, 1],
mode='markers',
marker=dict(size=8, color=y_np, colorscale='RdYlBu_r',
cmin=vmin, cmax=vmax,
line=dict(width=1, color='black')),
text=[f'Price: {price:.1f}' for price in y_np],
hoverinfo='x+y+text',
showlegend=False
),
row=2, col=2
)
# Layout
fig.update_layout(
title_text='Boston Housing Regression: Polynomial vs. MLPRegressor',
height=1000, width=1000,
margin=dict(l=50, r=50, t=50, b=50),
scene1=dict(
xaxis_title='RM (Rooms)',
yaxis_title='LSTAT (% Lower Status)',
zaxis_title='Price ($1000s)',
camera=dict(eye=dict(x=-1.5, y=-1.5, z=1))
),
scene2=dict(
xaxis_title='RM (Rooms)',
yaxis_title='LSTAT (% Lower Status)',
zaxis_title='Price ($1000s)',
camera=dict(eye=dict(x=-1.5, y=-1.5, z=1))
),
xaxis3=dict(title='RM (Rooms)'),
yaxis3=dict(title='LSTAT (% Lower Status)'),
xaxis4=dict(title='RM (Rooms)'),
yaxis4=dict(title='LSTAT (% Lower Status)'),
showlegend=False
)
fig.show()
# Predict and calculate MSE
y_pred_poly = polyreg.predict(X_np)
y_pred_mlp = mlp.predict(X_scaled)
mse_poly = mean_squared_error(y_np, y_pred_poly)
mse_mlp = mean_squared_error(y_np, y_pred_mlp)
# Print MSE for both models
print(f"Polynomial Regression MSE: {mse_poly:.2f}")
print(f"MLPRegressor MSE: {mse_mlp:.2f}")
Polynomial Regression MSE: 20.49 MLPRegressor MSE: 18.29
Backpropagation is the learning engine of modern neural networks.
Backpropagation makes it possible to train deep neural networks efficiently.
It allows a model to adjust millions of parameters efficiently by measuring how each one contributes to the overall error.
At its core it’s nothing more than the multivariate chain rule applied cleverly.
The key idea is simple:
Let’s begin with a single-layer model:
$$ \hat{y} = b_0 + b_1 x $$$$ E = \frac{1}{2}(\hat{y} - y)^2 $$Using the chain rule:
$$ \frac{\partial E}{\partial b_0} = \frac{\partial E}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial b_0} = (\hat{y} - y)(1) = \hat{y} - y $$$$ \frac{\partial E}{\partial b_1} = \frac{\partial E}{\partial \hat{y}} \cdot \frac{\partial \hat{y}}{\partial b_1} = (\hat{y} - y) x $$This is already backpropagation in its simplest form: one forward computation of the prediction and one backward computation of the gradient.
Consider a one-hidden-layer neural network:
$$ h = g(W_1 x + b_1) $$$$ \hat{y} = W_2 h + b_2 $$$$ E = \frac{1}{2}(\hat{y} - y)^2 $$Here $g(\cdot)$ is a nonlinear activation function.
To find $\frac{\partial E}{\partial W_1}$, we apply the chain rule through multiple layers:
We compute:
Each derivative is local and easy to compute.
Multiplying them together gives the gradient with respect to $W_1$.
The same procedure applies to all weights.
If we were to apply the chain rule naively, we’d expand the whole composition of functions explicitly.
For a deep network, the number of terms grows exponentially — this is what’s called the formula size.
Instead, backpropagation:
This means the cost of computing all gradients is linear in the size of the computation graph (the circuit size), not exponential in the size of the formula.
This is precisely the reason why neural networks are trainable in practice.
Backpropagation is not a special trick invented just for neural networks.
It’s the reverse-mode of automatic differentiation (AD):
This is why modern frameworks like PyTorch, TensorFlow, and JAX can compute gradients efficiently and automatically.
Everything comes down to this:
Backpropagation is nothing more than the chain rule for derivatives applied to a computation graph in a reverse accumulation order.
By caching intermediate values and local derivatives, we avoid recomputation and make training large models tractable.
Forward pass: x → h1 → h2 → y
↓
Backward pass: ∂E/∂y → ∂E/∂h2 → ∂E/∂h1
| Step | Description | Computation |
|---|---|---|
| Forward | Compute output (ŷ) and cache intermediates | O(circuit size) |
| Compute error | Loss function E(ŷ, y) | Scalar |
| Backward | Propagate error using chain rule | O(circuit size) |
| Update | Adjust parameters with gradient descent | W ← W − η ∇E |
The chain rule is the mathematical foundation.
The computation graph provides an efficient way to organize intermediate results.
Backpropagation is reverse-mode accumulation of derivatives through this graph.
Computational cost scales with the size of the graph, not the size of the expanded formula.
In short:
Backpropagation = Chain Rule + Smart Bookkeeping
That clever bookkeeping is what makes it possible for deep networks to learn from massive datasets in reasonable time.
Backpropagation is elegant because:
This little bit of math is the engine that powers modern deep learning.